#!/bin/bash
#
#
#
#
#

VERSION="9"
MAINTAINER="Christoph.Schmuecking@aeb.de"
UPDATEURL="http://update.aeb.de/scripts/backup"

# Changelog:
#
# 12/07/23 -	Added switch for download of example config
#		Change compression settings to xz, lzma, gzip, bzip2, none -- also bug fix
#
# 12/07/18 -	Added program check
#		Encryption optional - missing encryption shows warn msg
#		Added silent switch to suppress warn msgs
#		Generate filename
#		Show configured sources
#		Get just one source with -c
#		Added switch for compress algorithm
#
# 12/06/08 -	Added more remote storage (REMOTEMODE=file)
#		file - stores to any local directory
#		Updated die() function - write to stderr now
#		Updated update function to show readme or changelog after update
#
# 12/06/05 -	FTPCONFIG to TRANSFERCONFIG
#		Added modular remote host storage (ftp,smb)
#		Change FTP* to REMOTE*
#		Change FTPSERVER to REMOTEADDR
#		Added REMOTEMODE=[smb|ftp]
#		Better update function
#		Updated showHelp()
#
# 12/06/01 -	Added compress exclude option '_ONE-FILE-SYSTEM_'
#
# 12/05/08 -	Added update function
#		Updated showHelp() for update
#		Changed version string to a simple interger format
#
# 12/05/03 -	Added MySQL Support
#		Updated showHelp() for SQL compress and extract
#
# 12/04/30 -	Added delete support for files older and by name
#		Changed -d to -v for debug output
#		Changed file listing to better format
#		Added MAINTAINER info
#		Updated showHelp()
#
# 12/04/19 -	First release
#
# Changelog

function showHelp() {
	echo "backup script ${VERSION} - ${MAINTAINER}"
	echo ""
	echo "Common parameter:"
	echo " -v		- show debug info"
	echo " -h		- show this help"
	echo " -s		- file to gpg passphrase"
	echo " -u		- look for updated version"
	echo " -q		- do not show warn messages"
	echo " -a ALGO	- Change compression to ALGO"
	echo "		  Supported are: xz, lzma, gzip, bzip2, none"
	echo " -i		- download examples for config"
	echo ""
	echo "Compress data:"
	echo "$0 -m c -t TRANSFERCONFIG -b BACKUPCONFIG [ -s CRYPTKEY ] [ -a COMPRESSION ] [ -v ] [ -c SOURCE_NAME|_MYSQL_[:specific_db] ]"
	echo ""
	echo ""
	echo "Extract data:"
	echo "$0 -m e -t TRANSFERCONFIG -b BACKUPCONFIG [ -s CRYPTKEY ] [ -a COMPRESSION ] [ -v ] -e file-on-server.extension:path-to-extract-to[:specific-file-to-extract]"
	echo "path-to-extract must be '_MYSQL_' for MySQL extraction"
	echo ""
	echo ""
	echo "List data:"
	echo "$0 -m l -t TRANSFERCONFIG -b BACKUPCONFIG [ -s CRYPTKEY ] [ -a COMPRESSION ] [ -v ] [-l file-on-server.extension]"
	echo "Without '-l' the remote directory will be listed, otherwise the content of the file"
	echo ""
	echo ""
	echo "Delete data:"
	echo "$0 -m d -t TRANSFERCONFIG -b BACKUPCONFIG [ -s CRYPTKEY ] [ -a COMPRESSION ] [ -v ] -d file-on-server.extension|+seconds-older-then-now"
	echo ""
	echo ""
	echo "Show data:"
	echo "$0 -m s -t TRANSFERCONFIG -b BACKUPCONFIG [ -s CRYPTKEY ] [ -v ]"
	echo ""
	echo ""
	echo "TRANSFERCONFIG:"
	echo " Need: REMOTEMODE REMOTEADDR REMOTEPATH REMOTEUSER REMOTEPASS"
	echo " All values are strings"
	echo "  REMOTEMODE=smb|ftp|file"
	echo "  REMOTEPASS='PASS'"
	echo ""
	echo "BACKUPCONFIG:"
	echo " Need: ID SOURCES SQLDBSOURCE"
	echo "  ID=\"[A-Za-z0-9.-]\""
	echo "  SOURCES=\"name1 name2 _MYSQL_\""
	echo "  SQLDBSOURCE=\"DB1 DB2\""
	echo " Dependencies to SOURCES list:"
	echo "  value-from-source_path as string"
	echo "  name1_path=\"/path/to/\""
	echo " Optional:"
	echo "  value-from-source_exclude as string"
	echo "  name1_exclude=\"/path/to/NO\""
	echo "  specific values:"
	echo "   _ONE-FILE-SYSTEM_ wont follow binds or external mounts"
	echo "   name1_exclude=\"_ONE-FILE-SYSTEM_ /path/to/NO\""
	echo " Dependecies to SQLDBSOURCE list:"
	echo "  SQLDBSOURCE need trigger '_MYSQL_' in SOURCES"
	echo "  SQLDBSOURCE=\"DB1 DB2\""
	echo "  SQLDBUSER as string"
	echo "  SQLDBPASS as string"
	echo ""
	echo "CRYPTKEY:"
	echo " Need: CRYPTKEY"
	echo " Text from that file will be used as cryptkey"
	echo ""
	echo ""
	exit 0
}

function checkPrograms() {
	local baseprograms="tempfile readlink awk grep wget rm xargs head tail cat tar less"
	local ftpprograms="curl"
	local smbprograms="smbclient"
	local mysqlprograms="mysqldump"
	local cryptprograms="gpg"
	local compgzipprograms="gzip gunzip"
	local compbzip2programs="bzip2 bunzip2"
	local compxzprograms="xz unxz"

	local programs="${baseprograms}"
	if [ "-${cryptkey}" == "-null" ]; then programs+=" ${cryptprograms}"; fi
	local transferprograms="${REMOTEMODE}programs"
	programs+=" ${!transferprograms}"

	local compprograms="comp$(getCompressionType type)programs"
	programs+=" ${!compprograms}"


	for cmd in ${programs}; do
		command -v ${cmd} >/dev/null 2>&1 || die "Program ${cmd} is required"
	done
}

function doUpdate() {
	echo -ne "Locking for update..."
	tf="$(tempfile)"
	wget -qqq -O${tf} ${UPDATEURL}/latest
	v="$(grep -E '^VERSION="' ${tf} | awk -F\" '{print $2}')"
	if (( ${v} > ${VERSION} )); then
		echo -ne "updating..."
		script="$(readlink -f $0)"
		cp ${tf} ${script}
		wget -qqq -O${tf} ${UPDATEURL}/readme-v${v}
		if [ -s ${tf} ]; then
			less ${tf}
		fi
	fi
	rm ${tf}
	echo "ok."
	exit 0
}

function getConfigExample() {
	echo -ne "Locking for update..."
	tf="$(tempfile)"
	wget -qqq -O${tf} ${UPDATEURL}/example-config.tar
	dest="$(dirname $(readlink -f $0))"
	tar -xpf ${tf} -C ${dest}
	rm ${tf}
	echo "ok."
	exit 0
}

mode=0 #0=list; 1=comp; 2=extract; 3=delete; 4=show
debug=0
quiet=0
notreally=0
transferconfig=""
buconfig=""
cryptkey="null"

compressinfo=""
extractinfo=""
listinfo=""
delinfo=""
compalgo=""

timestamp="$(date +%s)"

while getopts :m:t:b:s:e:l:d:c:a:vuhqni option; do
	case "$option" in
		m)
			case "${OPTARG}" in
				l)	mode=0				;;
				c)	mode=1				;;
				e)	mode=2				;;
				d)	mode=3				;;
				s)	mode=4				;;
			esac
			;;
		t)	transferconfig="${OPTARG}"			;;
		b)	buconfig="${OPTARG}"				;;
		s)	cryptkey="${OPTARG}"				;;
		e)	extractinfo="${OPTARG}"				;;
		l)	listinfo="${OPTARG}"				;;
		d)	delinfo="${OPTARG}"				;;
		c)	compressinfo="${OPTARG}"			;;
		v)	let debug++					;;
		q)	let quiet++					;;
		h)	showHelp					;;
		u)	doUpdate					;;
		n)	notreally=1					;;
		a)	compalgo="${OPTARG}"				;;
		i)	getConfigExample				;;
		\?)	echo "unknown setting: $OPTARG"			;;
		:)	echo "missing argument in setting $OPTARG"	;;
	esac
done
shift $((OPTIND - 1))

function checkVariables() {
	if [ "-${REMOTEMODE}" == "-" ]; then die "Please configure a modus for remote transfer"; fi
	if [ "-${REMOTEUSER}" == "-" ]; then die "Please configure a remote username"; fi
	if [ "-${REMOTEPASS}" == "-" ]; then die "Please configure a remote password"; fi
	if [ "-${REMOTEADDR}" == "-" ]; then die "Please configure a remote server"; fi
	if [ "-${REMOTEPATH}" == "-" ]; then die "Please configure a remote path"; fi
	if [ "-${ID}" == "-" ]; then die "Please configure an ID"; fi
	if [ "-${SOURCES}" == "-" ]; then die "Please configure at least one source"; fi
}

function getTransferCommand() {
	local cmd="${1}${REMOTEMODE}"
	local file="$2"
	case "${cmd}" in
		listftp)	curl -sS -u ${REMOTEUSER}:${REMOTEPASS} ftp://${REMOTEADDR}${REMOTEPATH}/${ID}/|xargs -Il -n1 echo "l"|awk '{print $9" "$5}'						;;
		putftp)		curl -sS --ftp-create-dirs -u ${REMOTEUSER}:${REMOTEPASS} ftp://${REMOTEADDR}${REMOTEPATH}/${ID}/${file} -T -		;;
		getftp)		curl -sS -u ${REMOTEUSER}:${REMOTEPASS} ftp://${REMOTEADDR}${REMOTEPATH}/${ID}/${file}					;;
		delftp)		curl -sS -u ${REMOTEUSER}:${REMOTEPASS} -Q "-DELE ${file}" ftp://${REMOTEADDR}${REMOTEPATH}/${ID}/ > /dev/null		;;

		listsmb)	smbclient -l /dev/null -q //${REMOTEADDR}/${REMOTEPATH} ${REMOTEPASS} -U ${REMOTEUSER} -c "ls ${ID}/*"|xargs -Il -n1 echo "l"|awk '{print $1" "$3}'|head -n-1|tail -n+3	;;
		putsmb)		smbclient -l /dev/null -q -E //${REMOTEADDR}/${REMOTEPATH} ${REMOTEPASS} -U ${REMOTEUSER} -c "cd ${ID}; put - ${file}"	;;
		getsmb)		smbclient -l /dev/null -q -E //${REMOTEADDR}/${REMOTEPATH} ${REMOTEPASS} -U ${REMOTEUSER} -c "cd ${ID}/; get ${file} -"	;;
		delsmb)		smbclient -l /dev/null -q -E //${REMOTEADDR}/${REMOTEPATH} ${REMOTEPASS} -U ${REMOTEUSER} -c "cd ${ID}/; rm ${file}"	;;

		listfile)	ls -las ${REMOTEPATH}/${ID}/|xargs -Il -n1 echo "l"|awk '{print $9" "$6}'|tail -n+4					;;
		putfile)	cat > ${REMOTEPATH}/${ID}/${file}											;;
		getfile)	cat ${REMOTEPATH}/${ID}/${file}												;;
		delfile)	rm -f ${REMOTEPATH}/${ID}/${file} 											;;

		*)		die "no such protocol"													;;
	esac
}

function getCryptCommand() {
	local cmd="${1}"
	if [ "-${cryptkey}" == "-null" ]; then cat; return; fi
	case "${cmd}" in
		e)	gpg --batch --no-tty -q --passphrase-fd 9 9<${cryptkey} -c -			;;
		d)	gpg --batch --no-tty -q --passphrase-fd 9 9<${cryptkey} -d - 2>/dev/null	;;
	esac
}

function getCompressionType() {
	local cmd="${1}"
	local comp="xz"
	if [ "-${COMPRESSION}" != "-" ]; then	comp="${COMPRESSION}"	; fi
	if [ "-${compalgo}" != "-" ]; then	comp="${compalgo}"	; fi
	case "${cmd}${comp}" in
		extbzip2)	echo ".bz2"					;;
		typebzip2)	echo "bzip2"					;;
		extgzip)	echo ".gz"					;;
		typegzip)	echo "gzip"					;;
		extxz)		echo ".xz"					;;
		typexz)		echo "xz"					;;
		extlzma)	echo ".lzma"					;;
		typelzma)	echo "lzma"					;;
		extnone)	echo ""						;;
		typenone)	echo "none"					;;
		*)		die "Unsopported compression algorithm."	;;
	esac
}

function getFilename() {
	local type="${1}"
	local name="${2}"

	local crypt=".gpg"
	if [ "-${cryptkey}" == "-null" ]; then crypt=""; fi

	local comp="$(getCompressionType ext)"
	case "${type}" in
		sql)	echo "${name}_${timestamp}.sql${comp}${crypt}"			;;
		file)	echo "${name}_${timestamp}.tar${comp}${crypt}"			;;
	esac
}

function getCompressionCommand() {
	local cmd="${1}"
	local type="$(getCompressionType type)"
	case "${cmd}${type}" in
		targzip)	echo "--gzip"					;;
		compcmdgzip)	gzip						;;
		extcmdgzip)	gzip -d -c					;;
		tarbzip2)	echo "--bzip2"					;;
		compcmdbzip2)	bzip2 -z -c					;;
		extcmdbzip2)	bzip2 -c -d					;;
		tarxz)		echo "--xz"					;;
		compcmdxz)	xz -z -c					;;
		extcmdxz)	xz -d -c					;;
		tarlzma)	echo "--lzma"					;;
		compcmdlzma)	lzma -z -c					;;
		extcmdlzma)	lzma -d -c					;;
		tarnone)	echo ""						;;
		compcmdnone)	cat						;;
		extcmdnone)	cat						;;
		*)		die "Unsopported compression algorithm."	;;
	esac
}

function doListing() {
	if [ "-${listinfo}" == "-" ]; then
		local c=0
		local size name
		for f in $(getTransferCommand list); do
			let c++
			if (( $c == 1 )); then name="${f}"; fi
			if (( $c == 2 )); then
				size="${f}"
				c=0
				d="$(echo $name|awk -F_ '{print $(NF)}'|awk -F. '{print $1}')"
				echo "File: $name"
				echo -ne "Date: $(date -d @${d})\tSize: $size\n\n";
			fi
		done
	else
		getTransferCommand get ${listinfo} | getCryptCommand d | tar -tvpf - $(getCompressionCommand tar)
	fi
}

function doMYSQLCompress() {
	local db="${1}"
	printDebug "Compress MySQL"
	if [ "-${SQLDBSOURCE}" == "-" ]; then die "Please configure at least one DB Source"; fi
	if [ "-${SQLDBUSER}" == "-" ]; then die "Please configure a DB user"; fi
	if [ "-${SQLDBPASS}" == "-" ]; then die "Please configure a DB pass"; fi
	if [ "-${db}" != "-" ]; then SQLDBSOURCE="${db}"; fi
	for database in ${SQLDBSOURCE}; do
		printDebug "Dumping MySQL DB: ${database} "
		mysqldump -u ${SQLDBUSER} -p${SQLDBPASS} --single-transaction --databases ${database} | getCompressionCommand compcmd | getCryptCommand e | getTransferCommand put $(getFilename sql ${database})
	done
}

function doCompress() {
	local arrinfo=(${compressinfo//:/ })
	if [ "-${arrinfo[0]}" != "-" ]; then
		SOURCES=${arrinfo[0]};
        fi

	local verbose=""
	for s in ${SOURCES}; do
		printDebug "Compress ${s}"
		if [ "-${s}" == "-_MYSQL_" ]; then
			doMYSQLCompress ${arrinfo[1]}
			continue
		fi
		local ex=""
		local ofs=""
		p=$(eval echo \$${s}_path)
		e=$(eval echo \$${s}_exclude)

		if [ "-${p}" == "-" ]; then die "No such source configured!"; fi

		if [ "-${e}" != "-" ]; then
			for er in ${e}; do
				if [ "-${er}" == "-_ONE-FILE-SYSTEM_" ]; then
					ofs="--one-file-system"
					continue
				fi
				ex="${ex} --exclude=${er}"
			done
		fi
		if (( $debug >= 2 )); then verbose="v"; fi
		tar -c${verbose}pf - ${p} ${ex} ${ofs} $(getCompressionCommand tar) | getCryptCommand e | getTransferCommand put $(getFilename file ${s})
	done
}

function doMYSQLExtract() {
	printDebug "Extract MYSQL DB"
	getTransferCommand get ${arrinfo[0]} | getCryptCommand d | getCompressionCommand extcmd
}

function doExtract() {
	if [ "-${extractinfo}" == "-" ]; then die "Need extract informations."; fi
	local arrinfo=(${extractinfo//:/ })
	local file=""

	if [ "-${arrinfo[1]}" == "-_MYSQL_" ]; then
		doMYSQLExtract
		return
	fi

	case "${#arrinfo[@]}" in
		2)	printDebug "Extracting archive ${arrinfo[0]} to directory ${arrinfo[1]} ..."				;;
		3)	printDebug "Extracting file ${arrinfo[2]} from archive ${arrinfo[0]} to directory ${arrinfo[1]} ..."	;;
		*)	die "Cannot handle extract informations"								;;
	esac

	if [ -e  ${arrinfo[1]} ]; then
		# test for directory
		if [ -d ${arrinfo[1]} ]; then
			#test if empty
			if [ "$(ls -A ${arrinfo[1]})" ]; then
				die "Directory exists and is NOT empty."
			fi
		else
			die "${arrinfo[1]} is not a directory."
		fi
	else
		mkdir -p ${arrinfo[1]}
	fi

	if (( ${#arrinfo[@]} == 3 )); then file="${arrinfo[2]}"; fi
	getTransferCommand get ${arrinfo[0]} | getCryptCommand d | tar -xpf - -C  ${arrinfo[1]} $(getCompressionCommand tar) ${file}
}

function doDelete() {
	if [ "-${delinfo}" == "-" ]; then die "No information for deletions"; fi
	delinfo="${delinfo#"${delinfo%%[![:space:]]*}"}"
	if [ "${delinfo:0:1}" == "+" ]; then
		local deldate="$(echo $[ $(date +%s) - ${delinfo:1} ] )"
		local name=""
		local c=0
		for f in $(getTransferCommand list); do
			let c++
			if (( c == 2 )); then c=0; continue; fi
			name="${f}"
			d="$(echo $name|awk -F_ '{print $(NF)}'|awk -F. '{print $1}')"
			if (( $d <= $deldate )); then
				printDebug "Deleting file '${name}'"
				getTransferCommand del ${name}
			fi
		done
	else
		printDebug "Deleting file '${delinfo}'"
		getTransferCommand del ${delinfo}
	fi
}

function doConfigShow() {
	echo "Transfer:"
	echo -e "\tType:\t\t${REMOTEMODE}"
	echo -e "\tAddress:\t${REMOTEADDR}"
	echo -e "\tUser:\t\t${REMOTEUSER}"
	echo -e "\tPassword:\t${REMOTEPASS}"
	echo -e "\tPath:\t\t${REMOTEPATH}"
	echo -e "\tID:\t\t${ID}"

	echo "Sources:"
	for s in ${SOURCES}; do
		if [ "-${s}" == "-_MYSQL_" ]; then
			echo -e "\tName:\t\tMYSQL"
			echo -e "\tDatabases:\t${SQLDBSOURCE}"
			echo -e "\tUser:\t\t${SQLDBUSER}"
			echo -e "\tPassword:\t${SQLDBPASS}"
			echo ""
			continue
		fi

		echo -e "\tName:\t\t${s}"
		local p=$(eval echo \$${s}_path)
		local e=$(eval echo \$${s}_exclude)

		echo -e "\tPath:\t\t${p}"
		if [ "-${e}" != "-" ]; then
			echo -e "\tExclude:\t${e}"
			for er in ${e}; do
				if [ "-${er}" == "-_ONE-FILE-SYSTEM_" ]; then
					echo "\tStays on file system."
					break
				fi
			done
		fi
		echo ""
        done
}

function printDebug() {
	if (( $debug == 1 )); then
		echo "DBG: $@"
	fi
}

function printWarn() {
	if (( $quiet == 0 )); then
		echo "WARN: $@"
	fi
}

function die() {
	echo -ne "ERROR: $@\n" >> /dev/stderr
	exit 1
}

printDebug "Loading Transfer Configuration..."
if [ -e "${transferconfig}" -a -r "${transferconfig}" ]; then
	source ${transferconfig}
else
	die "Could not read transfer config."
fi

printDebug "Load Backup Configuration..."
if [ -e "${buconfig}" -a -r "${buconfig}" ]; then
	source ${buconfig}
else
	die "Could not read backup config."
fi


if [ "-${cryptkey}" != "-null" ]; then
	printDebug "Checking cypher key..."
	if [ ! -e "${cryptkey}" ] || [ ! -r "${cryptkey}" ]; then
		die "Could not read cypher key."
	fi
else
	printWarn "There is NO encryption configured."
	printWarn "All created backups will be plain text!"
	printWarn "All backups with encryption cannot be read!"
fi

checkVariables
checkPrograms

case $mode in
	0)	doListing	;;
	1)	doCompress	;;
	2)	doExtract	;;
	3)	doDelete	;;
	4)	doConfigShow	;;
esac

exit 0
